/****************************************************************************** * Copyright (C) Ultraleap, Inc. 2011-2021. * * * * Use subject to the terms of the Apache License 2.0 available at * * http://www.apache.org/licenses/LICENSE-2.0, or another agreement * * between Ultraleap and you, your company or other organization. * ******************************************************************************/ using Leap.Unity.Attributes; using System; using UnityEngine; using UnityEngine.Events; using UnityEngine.Serialization; namespace Leap.Unity.Interaction { /// /// A physics-enabled button. Activated by physically pressing the button, with events /// for press and unpress. /// public class InteractionButton : InteractionBehaviour { #region Inspector [Header("UI Control")] [Tooltip("When set to false, this UI control will not be functional. Use this instead " + "of disabling the component itself when you want to disable the user's " + "ability to affect this UI control while keeping the GameObject active and, " + "for example, rendering, and able to receive primaryHover state.")] [SerializeField, FormerlySerializedAs("controlEnabled")] private bool _controlEnabled = true; public bool controlEnabled { get { return _controlEnabled; } set { _controlEnabled = value; } } [Header("Motion Configuration")] [EditTimeOnly] public StartingPositionMode startingPositionMode = StartingPositionMode.Depressed; public enum StartingPositionMode { Depressed, Relaxed } /// /// The minimum and maximum heights the button can exist at. /// [Tooltip("The minimum and maximum heights the button can exist at.")] public Vector2 minMaxHeight = new Vector2(0f, 0.02f); /// /// The height that this button rests at; this value is a lerp in between the min and /// max height. /// [Tooltip("The height that this button rests at; this value is a lerp in between the min and max height.")] [Range(0f, 1f)] public float restingHeight = 0.5f; /// /// The spring force appied to the button to return it to its resting height. /// [Range(0, 1)] [SerializeField] private float _springForce = 0.1f; public float springForce { get { return _springForce; } set { _springForce = value; } } #endregion #region Events [SerializeField] [FormerlySerializedAs("OnPress")] private UnityEvent _OnPress = new UnityEvent(); [SerializeField] [FormerlySerializedAs("OnUnpress")] private UnityEvent _OnUnpress = new UnityEvent(); public Action OnPress = () => { }; public Action OnUnpress = () => { }; #endregion #region State protected bool _isPressed = false; /// Gets whether the button is currently held down. public bool isPressed { get { return _isPressed; } } [Obsolete("Deprecated. Use isPressed instead.", false)] public bool isDepressed { get { return _isPressed; } } protected bool _pressedThisFrame = false; /// /// Gets whether the button was pressed during this Update frame. /// public bool pressedThisFrame { get { return _pressedThisFrame; } } [Obsolete("Deprecated. Use pressedThisFrame instead.", false)] public bool depressedThisFrame { get { return _pressedThisFrame; } } protected bool _unpressedThisFrame = false; /// /// Gets whether the button was unpressed this frame. /// public bool unpressedThisFrame { get { return _unpressedThisFrame; } } [Obsolete("Deprecated. Use unpressedThisFrame instead.", false)] public bool unDepressedThisFrame { get { return _unpressedThisFrame; } } private float _pressedAmount = 0F; /// /// Gets a normalized value between 0 and 1 based on how depressed the button /// currently is relative to its maximum depression. 0 represents a button fully at /// rest or pulled out beyond its resting position; 1 represents a fully-pressed /// button. /// public float pressedAmount { get { return _pressedAmount; } } [Obsolete("Deprecated. Use pressedAmount instead.", false)] public float depressedAmount { get { return _pressedAmount; } } /// /// The initial position of this element in local space, stored on Start(). /// protected Vector3 initialLocalPosition; /// /// The physical position of this element in local space; may diverge from the /// graphical position. /// protected Vector3 localPhysicsPosition; /// /// The physical position of this element in world space; may diverge from the /// graphical position. /// protected Vector3 physicsPosition = Vector3.zero; /// /// Returns the local position of this button when it is able to relax into its target /// position. /// public virtual Vector3 RelaxedLocalPosition { get { return initialLocalPosition + Vector3.back * Mathf.Lerp(minMaxHeight.x, minMaxHeight.y, restingHeight); } } private Rigidbody _lastDepressor; private Vector3 _localDepressorPosition; private Vector3 _physicsVelocity = Vector3.zero; private bool _physicsOccurred; private bool _initialIgnoreGrasping = false; private Quaternion _initialLocalRotation; private InteractionController _lockedInteractingController = null; #endregion #region Unity Events void Reset() { contactForceMode = ContactForceMode.UI; graspedMovementType = GraspedMovementType.Nonkinematic; startingPositionMode = StartingPositionMode.Relaxed; rigidbody = GetComponent(); if (rigidbody != null) { rigidbody.useGravity = false; } } protected override void OnDisable() { if (isPressed) { _unpressedThisFrame = true; OnUnpress(); if (_lockedInteractingController != null) { _lockedInteractingController.primaryHoverLocked = false; } } base.OnDisable(); } protected override void Start() { if (transform == transform.root) { Debug.LogError("This button has no parent! Please ensure that it is parented to something!", this); enabled = false; } // Initialize Positions initialLocalPosition = transform.localPosition; if (startingPositionMode == StartingPositionMode.Relaxed) { initialLocalPosition = transform.localPosition + Vector3.forward * Mathf.Lerp(minMaxHeight.x, minMaxHeight.y, restingHeight); } transform.localPosition = initialLocalPosition + Vector3.back * Mathf.Lerp(minMaxHeight.x, minMaxHeight.y, restingHeight); localPhysicsPosition = transform.localPosition; physicsPosition = transform.position; rigidbody.position = physicsPosition; _initialIgnoreGrasping = ignoreGrasping; _initialLocalRotation = transform.localRotation; //Add a custom grasp controller OnGraspBegin += onGraspBegin; OnGraspEnd += onGraspEnd; OnPress += _OnPress.Invoke; OnUnpress += _OnUnpress.Invoke; base.Start(); } protected virtual void FixedUpdate() { if (!_physicsOccurred) { _physicsOccurred = true; if (!isGrasped && !rigidbody.IsSleeping()) { float localPhysicsDisplacementPercentage = Mathf.InverseLerp(minMaxHeight.x, minMaxHeight.y, initialLocalPosition.z - localPhysicsPosition.z); // Sleep the rigidbody if it's not really moving. if (rigidbody.position == physicsPosition && _physicsVelocity == Vector3.zero && Mathf.Abs(localPhysicsDisplacementPercentage - restingHeight) < 0.01F) { rigidbody.Sleep(); } else { // Otherwise reset the body's position to where it was last time PhysX // looked at it. if (_physicsVelocity.ContainsNaN()) { _physicsVelocity = Vector3.zero; } rigidbody.position = physicsPosition; rigidbody.velocity = _physicsVelocity; } } } } private const float FRICTION_COEFFICIENT = 30F; private const float DRAG_COEFFICIENT = 60F; protected virtual void Update() { // Reset our convenience state variables. _pressedThisFrame = false; _unpressedThisFrame = false; // Disable collision on this button if it is not the primary hover. ignoreGrasping = _initialIgnoreGrasping ? true : !isPrimaryHovered && !isGrasped; ignoreContact = (!isPrimaryHovered || isGrasped) || !controlEnabled; // Enforce local rotation (if button is child of non-kinematic rigidbody, // this is necessary). transform.localRotation = _initialLocalRotation; // Apply physical corrections only if PhysX has modified our positions. if (_physicsOccurred) { _physicsOccurred = false; // Record and enforce the sliding state from the previous frame. if (this.primaryHoverDistance < 0.005f || isGrasped) { localPhysicsPosition = constrainDepressedLocalPosition( transform.parent.InverseTransformPoint(rigidbody.position) - localPhysicsPosition); } else { Vector2 localSlidePosition = new Vector2(localPhysicsPosition.x, localPhysicsPosition.y); localPhysicsPosition = transform.parent.InverseTransformPoint(this.transform.position); localPhysicsPosition = new Vector3(localSlidePosition.x, localSlidePosition.y, localPhysicsPosition.z); } // Calculate the physical kinematics of the button in local space Vector3 localPhysicsVelocity = transform.parent.InverseTransformVector(rigidbody.velocity); if (isPressed && isPrimaryHovered && _lastDepressor != null) { Vector3 curLocalDepressorPos = transform.parent.InverseTransformPoint(_lastDepressor.position); Vector3 origLocalDepressorPos = transform.parent.InverseTransformPoint(transform.TransformPoint(_localDepressorPosition)); localPhysicsVelocity = Vector3.back * 0.05f; localPhysicsPosition = constrainDepressedLocalPosition(curLocalDepressorPos - origLocalDepressorPos); } else if (isGrasped) { // Do nothing! } else { Vector3 originalLocalVelocity = localPhysicsVelocity; // Spring force localPhysicsVelocity += Mathf.Clamp(_springForce * 10000F * (initialLocalPosition.z - Mathf.Lerp(minMaxHeight.x, minMaxHeight.y, restingHeight) - localPhysicsPosition.z), -100f / transform.parent.lossyScale.x, 100f / transform.parent.lossyScale.x) * Time.fixedDeltaTime * Vector3.forward; // Friction & Drag float velMag = originalLocalVelocity.magnitude; var frictionDragVelocityChangeAmt = 0f; if (velMag > 0F) { // Friction force var frictionForceAmt = velMag * FRICTION_COEFFICIENT; frictionDragVelocityChangeAmt += Time.fixedDeltaTime * transform.parent.lossyScale.x * frictionForceAmt; // Drag force float velSqrMag = velMag * velMag; var dragForceAmt = velSqrMag * DRAG_COEFFICIENT; frictionDragVelocityChangeAmt += Time.fixedDeltaTime * transform.parent.lossyScale.x * dragForceAmt; // Apply velocity change, but don't let friction or drag let velocity // magnitude cross zero. var newVelMag = Mathf.Max(0, velMag - frictionDragVelocityChangeAmt); localPhysicsVelocity = localPhysicsVelocity / velMag * newVelMag; } } // Transform the local physics back into world space physicsPosition = transform.parent.TransformPoint(localPhysicsPosition); _physicsVelocity = transform.parent.TransformVector(localPhysicsVelocity); // Calculate the Depression State of the Button from its Physical Position // Set its Graphical Position to be Constrained Physically bool oldDepressed = isPressed; // Normalized depression amount. _pressedAmount = localPhysicsPosition.z.Map(initialLocalPosition.z - minMaxHeight.x, initialLocalPosition.z - Mathf.Lerp(minMaxHeight.x, minMaxHeight.y, restingHeight), 1F, 0F); // If the button is depressed past its limit... if (localPhysicsPosition.z > initialLocalPosition.z - minMaxHeight.x) { transform.localPosition = new Vector3(localPhysicsPosition.x, localPhysicsPosition.y, initialLocalPosition.z - minMaxHeight.x); if ((isPrimaryHovered && _lastDepressor != null) || isGrasped) { _isPressed = true; } else { physicsPosition = transform.parent.TransformPoint(new Vector3(localPhysicsPosition.x, localPhysicsPosition.y, initialLocalPosition.z - minMaxHeight.x)); _physicsVelocity = _physicsVelocity * 0.1f; _isPressed = false; _lastDepressor = null; } // Else if the button is extended past its limit... } else if (localPhysicsPosition.z < initialLocalPosition.z - minMaxHeight.y) { transform.localPosition = new Vector3(localPhysicsPosition.x, localPhysicsPosition.y, initialLocalPosition.z - minMaxHeight.y); physicsPosition = transform.position; _isPressed = false; _lastDepressor = null; } else { // Else, just make the physical and graphical motion of the button match transform.localPosition = localPhysicsPosition; // Allow some hysteresis before setting isDepressed to false. if (!isPressed || !(localPhysicsPosition.z > initialLocalPosition.z - (minMaxHeight.y - minMaxHeight.x) * 0.1F)) { _isPressed = false; _lastDepressor = null; } } // If our depression state has changed since last time... if (isPressed && !oldDepressed) { primaryHoveringController.primaryHoverLocked = true; _lockedInteractingController = primaryHoveringController; OnPress(); _pressedThisFrame = true; } else if (!isPressed && oldDepressed) { _unpressedThisFrame = true; OnUnpress(); if (!(isGrasped && graspingController == _lockedInteractingController)) { _lockedInteractingController.primaryHoverLocked = false; } _lastDepressor = null; } } } /// /// Clamps the input local-space position to the bounds allowed by this UI element, /// without clamping along the button depression axis. For buttons, this is locks the /// element in local-XY space, but not along the pressing axis (Z axis). /// protected virtual Vector3 constrainDepressedLocalPosition(Vector3 localPosition) { // Buttons are only allowed to move along their Z axis. return new Vector3( initialLocalPosition.x, initialLocalPosition.y, localPhysicsPosition.z + localPosition.z); } protected virtual void onGraspBegin() { primaryHoveringController.LockPrimaryHover(this); _lockedInteractingController = primaryHoveringController; } protected virtual void onGraspEnd() { if (localPhysicsPosition.z > initialLocalPosition.z - minMaxHeight.x) { transform.localPosition = new Vector3(localPhysicsPosition.x, localPhysicsPosition.y, initialLocalPosition.z - minMaxHeight.x); _physicsVelocity = _physicsVelocity * 0.1f; } if (_lockedInteractingController != null && !isPressed) { _lockedInteractingController.primaryHoverLocked = false; _lockedInteractingController = null; } } protected virtual void OnCollisionEnter(Collision collision) { trySetDepressor(collision.collider); } protected virtual void OnCollisionStay(Collision collision) { trySetDepressor(collision.collider); } // during Soft Contact, controller colliders are triggers protected virtual void OnTriggerEnter(Collider collider) { trySetDepressor(collider); } protected virtual void OnTriggerStay(Collider collider) { trySetDepressor(collider); } // Try grabbing the offset between the fingertip and this object... private void trySetDepressor(Collider collider) { if (collider.attachedRigidbody != null && _lastDepressor == null && (localPhysicsPosition.z > initialLocalPosition.z - minMaxHeight.x) && (manager.contactBoneBodies.ContainsKey(collider.attachedRigidbody) && !this.ShouldIgnoreHover(manager.contactBoneBodies[collider.attachedRigidbody].interactionController))) { _lastDepressor = collider.attachedRigidbody; _localDepressorPosition = transform.InverseTransformPoint(collider.attachedRigidbody.position); } } #endregion #region Gizmos protected virtual void OnDrawGizmosSelected() { if (transform.parent != null) { Gizmos.matrix = transform.parent.localToWorldMatrix; Vector2 heights = minMaxHeight; Vector3 originPosition = Application.isPlaying ? initialLocalPosition : transform.localPosition; if (!Application.isPlaying && startingPositionMode == StartingPositionMode.Relaxed) { originPosition = transform.localPosition + Vector3.forward * Mathf.Lerp(minMaxHeight.x, minMaxHeight.y, restingHeight); } Gizmos.color = Color.red; Gizmos.DrawLine(originPosition + (Vector3.back * heights.x), originPosition + (Vector3.back * heights.y)); Gizmos.color = Color.green; Gizmos.DrawLine(originPosition + (Vector3.back * heights.x), originPosition + (Vector3.back * Mathf.Lerp(heights.x, heights.y, restingHeight))); } } #endregion #region Public Methods /// /// Sets the minimum height (x component) of the minMaxHeight property. The minimum /// height can't be set larger than the maximum height with this method (it will be /// clamped if necessary). /// public void SetMinHeight(float minHeight) { minMaxHeight = new Vector2(Mathf.Min(minMaxHeight.y, minHeight), minMaxHeight.y); } [Obsolete("Deprecated. Use SetMinHeight instead.", false)] public void setMinHeight(float minHeight) { SetMinHeight(minHeight); } /// /// Sets the maximum height (y component) of the minMaxHeight property. The maximum /// height can't be set smaller than the minimum height with this method (it will be /// clamped if necessary). /// public void SetMaxHeight(float maxHeight) { minMaxHeight = new Vector2(minMaxHeight.x, Mathf.Max(minMaxHeight.x, maxHeight)); } [Obsolete("Deprecated. Use SetMaxHeight instead.", false)] public void setMaxHeight(float maxHeight) { SetMaxHeight(maxHeight); } #endregion } }